Leaflet. Дружим Image с Canvas / Хабр |
您所在的位置:网站首页 › leaflet map touch › Leaflet. Дружим Image с Canvas / Хабр |
![]() Доброго времени суток, дорогие хабрахабровцы! Leaflet — библиотека, позволяющая добавить интерактивные карты на Ваш сайт и легко их кастомизировать. Сегодня рассмотрим то, как можно разместить изображения на Canvas-слое карт, совместно с базовыми маркерами. Задача Построить трек с отметкой различных статусов состояния. Статусы отмечаются маркерами. У каждого статуса есть свой приоритет.Для оптимизации карты, рендеринг объектов должен происходить с использованием Canvas. Маркеры могут быть двух типов: точки и изображения. Если маркеры перекрывают друг друга — то сверху должен оказаться маркер более приоритетного статуса. Каждый маркер должен быть активным при наведении на него мышкой (например для вывода дополнительной информации). Подготовка Подключим библиотеку Leaflet.js и добавим базовую карту. const map = L.map('map', { preferCanvas: true, }).setView([51.505, -0.09], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); Для наглядности будем использовать 3 состояния в порядке увеличение приоритета: базовый (зеленый маркер), сообщение (изображение) и ошибка (красный маркер). Соответственно, красный маркер должен перекрывать изображение, а изображение — перекрывать зеленый маркер. /* Базовый маркер */ L.circleMarker(L.latLng(51.52, -0.109), { radius: 10, fillColor: '#27ae60', fillOpacity: 1, color: '#fff', weight: 3, }).addTo(map); /* Маркер сообщения */ L.marker(L.latLng(51.52, -0.109), { icon: L.icon({ iconUrl: 'icon.png', // url картинки iconSize: [40, 40], // размер маркера iconAnchor: [20, 20], // выравнивание относительно центра }), }).addTo(map); /* Маркер ошибки */ L.circleMarker(L.latLng(51.52, -0.109), { radius: 8, fillColor: '#f44334', fillOpacity: 1, color: '#fff', weight: 3, }).addTo(map); Проблема Leaflet добавляет маркеры поочередно, поэтому каждый последующий должен перекрывать предыдущий. Но на деле это не так. L.marker добавляет изображение в качестве обыкновенного IMG, отдельно от слоя Canvas. Его можно разместить либо перед, либо под Canvas. И как следствие, невозможно поместить L.marker между двух L.circleMarker. Следовательно, нужен способ размещать изображения в том же Canvas, на который добавляются и стандартные маркеры. Примечание: В сети есть несколько плагинов, позволяющих добавлять изображения на Canvas. Но они создают отдельный Canvas, или даже группу слоев! В итоге простое размещение маркеров по приоритету становится довольно затруднительным. А так же Canvas-слои перекрывают друг друга, и кликнуть мышкой на маркер нижестоящего слоя становится невозможным! Решение Шаг 1. Создаем дочерний класс от L.CircleMarker, который будет получать объект 'img', загружать изображение и добавлять его в L.Canvas. const CanvasMarker = L.CircleMarker.extend({ _updatePath() { if (!this.options.img.el) { //Создаем элемент IMG const img = document.createElement('img'); img.src = this.options.img.url; this.options.img.el = img; img.onload = () => { this.redraw(); //После загрузки запускаем перерисовку }; } else { this._renderer._updateImg(this); //Вызываем _updateImg } }, }); L.canvasMarker = function (...options) { return new CanvasMarker(...options); }; Шаг 2. Описываем метод _updateImg в L.Canvas. Он получает объект с изображением, который мы передаем на Шаге 1 и рисует его на Canvas. L.Canvas.include({ _updateImg(layer) { //Метод добавления img на Canvas-слой const { img } = layer.options; const p = layer._point.round(); this._ctx.drawImage(img.el, p.x - img.size[0] / 2, p.y - img.size[1] / 2, img.size[0], img.size[1]); }, }); Шаг 3. Теперь вместо L.marker можно использовать L.canvasMarker. Обратите внимание, что параметр 'anchor' не используется, т.к. картинка выравнивается автоматически! /* Базовый маркер */ L.circleMarker(L.latLng(51.52, -0.109), { radius: 10, fillColor: '#27ae60', fillOpacity: 1, color: '#fff', weight: 3, }).addTo(map); /* Маркер сообщения */ L.canvasMarker(L.latLng(51.52, -0.109), { img: { url: 'icon.png', size: [40, 40], }, }).addTo(map); /* Маркер ошибки */ L.circleMarker(L.latLng(51.52, -0.109), { radius: 8, fillColor: '#f44334', fillOpacity: 1, color: '#fff', weight: 3, }).addTo(map); В результате: Все маркеры расположены на едином Canvas-слое. Маркеры перекрывают друг-друга в порядке их добавления на карту. При наведении на маркеры мышкой, они сохраняют активность. Задача решена! Дополнительно Давайте «прокачаем» наш метод L.canvasMarker и добавим возможность автоматически разворачивать изображение в направлении движения по карте! За основу возьмем координаты предыдущей точки. Для этого сначала доработаем метод _updateImg. L.Canvas.include({ _updateImg(layer) { const { img } = layer.options; const p = layer._point.round(); if (img.rotate) { this._ctx.save(); this._ctx.translate(p.x, p.y); this._ctx.rotate(img.rotate * Math.PI / 180); this._ctx.drawImage(img.el, -img.size[0] / 2, -img.size[1] / 2, img.size[0], img.size[1]); this._ctx.restore(); } else { this._ctx.drawImage(img.el, p.x - img.size[0] / 2, p.y - img.size[1] / 2, img.size[0], img.size[1]); } }, }); Как видно из примера, для поворота у 'img' должно быть свойство 'rotate'. И мы уже можем задать его вручную при добавлении маркера: L.canvasMarker(L.latLng(51.52, -0.109), { img: { url: 'icon.png', size: [40, 40], rotate: 15, //угол поворота изображения }, }).addTo(map); Но нам нужно вычислять угол поворота автоматически на основе предыдущей точки. Поэтому добавим вычисление угла на основе двух координат (angleCrds): const angleCrds = (map, prevLatlng, latlng) => { if (!latlng || !prevLatlng) return 0; const pxStart = map.project(prevLatlng); const pxEnd = map.project(latlng); return Math.atan2(pxStart.y - pxEnd.y, pxStart.x - pxEnd.x) / Math.PI * 180 - 90; }; const CanvasMarker = L.CircleMarker.extend({ _updatePath() { if (!this.options.img.el) { /* Вызываем метод */ if (!this.options.img.rotate) this.options.img.rotate = 0; this.options.img.rotate += angleCrds(this._map, this.options.prevLatlng, this._latlng); const img = document.createElement('img'); img.src = this.options.img.url; this.options.img.el = img; img.onload = () => { this.redraw(); }; } else { this._renderer._updateImg(this); } }, }); L.canvasMarker(L.latLng(51.52, -0.109), { prevLatlng: L.latLng(51.528, -0.1), // Координаты предыдущей точки img: { url: 'icon.png', size: [40, 40], }, }).addTo(map); Заключение → Пример работы можно увидеть здесь → Весь описанный функционал я вынес в отдельный npm-плагин Этот плагин легко подключить и использовать в своих проектах! Так же плагин поддерживает дополнительные настройки, не описанные в данной статье. Спасибо за внимание! |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |